/*
 * Copyright 2017-2025 noear.org and authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.noear.solon.maven.plugin;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

/**
 * Utility used to run a process.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 * @author Dmytro Nosan
 * @since 1.1.0
 */
public class RunProcess {

    private static final long JUST_ENDED_LIMIT = 500;

    private final File workingDirectory;

    private final String[] command;

    private volatile Process process;

    private volatile long endTime;

    /**
     * Creates new {@link RunProcess} instance for the specified command.
     *
     * @param command the program to execute and its arguments
     */
    public RunProcess(String... command) {
        this(null, command);
    }

    /**
     * Creates new {@link RunProcess} instance for the specified working directory and
     * command.
     *
     * @param workingDirectory the working directory of the child process or {@code null}
     *                         to run in the working directory of the current Java process
     * @param command          the program to execute and its arguments
     */
    public RunProcess(File workingDirectory, String... command) {
        this.workingDirectory = workingDirectory;
        this.command = command;
    }

    public int run(boolean waitForProcess, String... args) throws IOException {
        return run(waitForProcess, Arrays.asList(args), Collections.emptyMap());
    }

    public int run(boolean waitForProcess, Collection<String> args, Map<String, String> environmentVariables)
            throws IOException {
        ProcessBuilder builder = new ProcessBuilder(this.command);
        builder.directory(this.workingDirectory);
        builder.command().addAll(args);
        builder.environment().putAll(environmentVariables);
        builder.redirectErrorStream(true);
        builder.inheritIO();
        try {
            Process process = builder.start();
            this.process = process;
            SignalUtils.attachSignalHandler(this::handleSigInt);
            if (waitForProcess) {
                try {
                    return process.waitFor();
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                    return 1;
                }
            }
            return 5;
        } finally {
            if (waitForProcess) {
                this.endTime = System.currentTimeMillis();
                this.process = null;
            }
        }
    }

    /**
     * Return the running process.
     *
     * @return the process or {@code null}
     */
    public Process getRunningProcess() {
        return this.process;
    }

    /**
     * Return if the process was stopped.
     *
     * @return {@code true} if stopped
     */
    public boolean handleSigInt() {
        if (allowChildToHandleSigInt()) {
            return true;
        }
        return doKill();
    }

    private boolean allowChildToHandleSigInt() {
        Process process = this.process;
        if (process == null) {
            return true;
        }
        long end = System.currentTimeMillis() + 5000;
        while (System.currentTimeMillis() < end) {
            if (!process.isAlive()) {
                return true;
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        return false;
    }

    /**
     * Kill this process.
     */
    public void kill() {
        doKill();
    }

    private boolean doKill() {
        // destroy the running process
        Process process = this.process;
        if (process != null) {
            try {
                process.destroy();
                process.waitFor();
                this.process = null;
                return true;
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
        return false;
    }

    public boolean hasJustEnded() {
        return System.currentTimeMillis() < (this.endTime + JUST_ENDED_LIMIT);
    }

}
